﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using System.Xml;
using System.Xml.XPath;
using Monti.MVVM;

namespace WikiUploader.Classes.DotNetWikiBot
{
   /// <summary>Class defines wiki page object.</summary>
   [ClassInterface (ClassInterfaceType.AutoDispatch)]
   [Serializable]
   public class WikiPage : ViewModelBase
   {
      private string m_PageUrl;
      private string m_PageId;
      private string m_Title;
      private string m_Text;
      private string m_LastUser;
      private string m_Comment;
      private DateTime m_Timestamp;

      /// <summary>Gets the page's title.</summary>
      public string Title
      {
         get { return this.m_Title; }
         set
         {
            this.m_Title = value;
            this.PageUrl = WikiSite.SiteUrl + WikiSite.IndexPath + "index.php?title=" + HttpUtility.UrlEncode (Title);
            this.RaisePropertyChanged ("Title");
         }
      }

      /// <summary>Gets the page's text.</summary>
      public string Text
      {
         get { return this.m_Text; }
         set
         {
            this.m_Text = value;
            this.RaisePropertyChanged ("Text");
         }
      }

      /// <summary>
      /// Gets the page's url.
      /// </summary>
      public string PageUrl
      {
         get { return this.m_PageUrl; }
         private set
         {
            this.m_PageUrl = value;
            this.RaisePropertyChanged ("PageUrl");
         }
      }

      /// <summary>Page ID in internal MediaWiki database.</summary>
      public string PageId
      {
         get { return this.m_PageId; }
         set
         {
            this.m_PageId = value;
            this.RaisePropertyChanged ("PageId");
         }
      }

      /// <summary>Username or IP-address of last page contributor.</summary>
      public string LastUser
      {
         get { return this.m_LastUser; }
         set
         {
            this.m_LastUser = value;
            this.RaisePropertyChanged ("LastUser");
         }
      }

      /// <summary>Last contributor ID in internal MediaWiki database.</summary>
      public string LastUserId { get; set; }

      /// <summary>Page revision ID in the internal MediaWiki database.</summary>
      public string LastRevisionId { get; set; }

      /// <summary>True, if last edit was minor edit.</summary>
      public bool LastMinorEdit { get; set; }

      /// <summary>Amount of bytes, modified during last edit.</summary>
      public int LastBytesModified { get; set; }

      /// <summary>Last edit comment.</summary>
      public string Comment
      {
         get { return this.m_Comment; }
         set
         {
            this.m_Comment = value;
            this.RaisePropertyChanged ("Comment");
         }
      }

      /// <summary>Date and time of last edit expressed in UTC (Coordinated Universal Time).
      /// Call "timestamp.ToLocalTime()" to convert to local time if it is necessary.</summary>
      public DateTime Timestamp
      {
         get { return this.m_Timestamp; }
         set
         {
            this.m_Timestamp = value;
            this.RaisePropertyChanged ("Timestamp");
         }
      }

      /// <summary>True, if this page is in bot account's watchlist. Call GetEditSessionData
      /// function to get the actual state of this property.</summary>
      public bool IsWatched { get; set; }

      /// <summary>This edit session time attribute is used to edit pages.</summary>
      public string EditSessionTime { get; set; }

      /// <summary>This edit session token attribute is used to edit pages.</summary>
      public string EditSessionToken { get; set; }

      /// <summary>Site, on which the page is.</summary>
      public WikiSite WikiSite { get; private set; }

      /// <summary>This constructor creates Page object with specified title and specified
      /// Site object. This is preferable constructor. When constructed, new Page object doesn't
      /// contain text. Use Load() method to get text from live wiki. Or use LoadEx() to get
      /// both text and metadata via XML export interface.</summary>
      /// <param name="wikiSite">Site object, it must be constructed beforehand.</param>
      /// <param name="title">Page title as string.</param>
      /// <returns>Returns Page object.</returns>
      public WikiPage (WikiSite wikiSite, string title)
      {
         this.PageId = "(Page does not exist yet.)";
         this.WikiSite = wikiSite;
         this.Title = title;
      }

      /// <summary>This constructor creates empty Page object with specified Site object,
      /// but without title. Avoid using this constructor needlessly.</summary>
      /// <param name="wikiSite">Site object, it must be constructed beforehand.</param>
      /// <returns>Returns Page object.</returns>
      public WikiPage (WikiSite wikiSite)
      {
         this.WikiSite = wikiSite;
      }

      /// <summary>This constructor creates Page object with specified title. Site object
      /// with default properties is created internally and logged in. Constructing
      /// new Site object is too slow, don't use this constructor needlessly.</summary>
      /// <param name="title">Page title as string.</param>
      /// <returns>Returns Page object.</returns>
      public WikiPage (string title)
      {
         this.WikiSite = new WikiSite ();
         this.Title = title;
      }

      /// <summary>This constructor creates Page object with specified page's numeric revision ID
      /// (also called "oldid"). Page title is retrieved automatically
      /// in this constructor.</summary>
      /// <param name="wikiSite">Site object, it must be constructed beforehand.</param>
      /// <param name="revisionId">Page's numeric revision ID (also called "oldid").</param>
      /// <returns>Returns Page object.</returns>
      public WikiPage (WikiSite wikiSite, Int64 revisionId)
      {
         if (revisionId <= 0)
            throw new ArgumentOutOfRangeException ("revisionId",
                                                  WikiBot.Msg ("Revision ID must be positive."));
         this.WikiSite = wikiSite;
         LastRevisionId = revisionId.ToString (CultureInfo.InvariantCulture);
         GetTitle ();
      }

      /// <summary>This constructor creates Page object with specified page's numeric revision ID
      /// (also called "oldid"). Page title is retrieved automatically in this constructor.
      /// Site object with default properties is created internally and logged in. Constructing
      /// new Site object is too slow, don't use this constructor needlessly.</summary>
      /// <param name="revisionId">Page's numeric revision ID (also called "oldid").</param>
      /// <returns>Returns Page object.</returns>
      public WikiPage (Int64 revisionId)
      {
         if (revisionId <= 0)
            throw new ArgumentOutOfRangeException ("revisionId",
                                                  WikiBot.Msg ("Revision ID must be positive."));
         this.WikiSite = new WikiSite ();
         LastRevisionId = revisionId.ToString (CultureInfo.InvariantCulture);
         GetTitle ();
      }

      /// <summary>This constructor creates empty Page object without title. Site object with
      /// default properties is created internally and logged in. Constructing new Site object
      /// is too slow, avoid using this constructor needlessly.</summary>
      /// <returns>Returns Page object.</returns>
      public WikiPage ()
      {
         this.WikiSite = new WikiSite ();
      }

      /// <summary>Loads actual page text for live wiki site via raw web interface.
      /// If Page.lastRevisionID is specified, the function gets that specified
      /// revision.</summary>
      public void Load ()
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to load."));
         string res = WikiSite.SiteUrl + WikiSite.IndexPath + "index.php?title=" +
                      HttpUtility.UrlEncode (Title) +
                      (string.IsNullOrEmpty (LastRevisionId) ? "" : "&oldid=" + LastRevisionId) +
                      "&redirect=no&action=raw&ctype=text/plain&dontcountme=s";
         try
         {
            Text = WikiSite.GetPageHtm (res);
         }
         catch (WebException e)
         {
            string message = e.Message;
            if (message.Contains (": (404) "))
            {		// Not Found
               Console.Error.WriteLine (WikiBot.Msg ("Page \"{0}\" doesn't exist."), Title);
               Text = "";
               return;
            }
            throw;
         }
         Console.WriteLine (WikiBot.Msg ("Page \"{0}\" loaded successfully."), Title);
      }

      /// <summary>Loads page text and metadata via XML export interface. It is slower,
      /// than Load(), don't use it if you don't need page metadata (page id, timestamp,
      /// comment, last contributor, minor edit mark).</summary>
      public void LoadEx ()
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to load."));
         string res = WikiSite.SiteUrl + WikiSite.IndexPath + "index.php?title=Special:Export/" +
                      HttpUtility.UrlEncode (Title) + "&action=submit";
         string src = WikiSite.GetPageHtm (res);
         ParsePageXml (src);
      }

      /// <summary>This internal function parses MediaWiki XML export data using XmlDocument
      /// to get page text and metadata.</summary>
      /// <param name="xmlSrc">XML export source code.</param>
      public void ParsePageXml (string xmlSrc)
      {
         var doc = new XmlDocument ();
         doc.LoadXml (xmlSrc);
         if (doc.GetElementsByTagName ("page").Count == 0)
         {
            Console.Error.WriteLine (WikiBot.Msg ("Page \"{0}\" doesn't exist."), Title);
            return;
         }
         Text = doc.GetElementsByTagName ("text")[0].InnerText;
         PageId = doc.GetElementsByTagName ("id")[0].InnerText;
         if (doc.GetElementsByTagName ("username").Count != 0)
         {
            LastUser = doc.GetElementsByTagName ("username")[0].InnerText;
            LastUserId = doc.GetElementsByTagName ("id")[2].InnerText;
         }
         else if (doc.GetElementsByTagName ("ip").Count != 0)
            LastUser = doc.GetElementsByTagName ("ip")[0].InnerText;
         else
            LastUser = "(n/a)";
         LastRevisionId = doc.GetElementsByTagName ("id")[1].InnerText;
         if (doc.GetElementsByTagName ("comment").Count != 0)
            Comment = doc.GetElementsByTagName ("comment")[0].InnerText;
         Timestamp = DateTime.Parse (doc.GetElementsByTagName ("timestamp")[0].InnerText);
         Timestamp = Timestamp.ToUniversalTime ();
         LastMinorEdit = (doc.GetElementsByTagName ("minor").Count != 0);
         if (string.IsNullOrEmpty (Title))
            Title = doc.GetElementsByTagName ("title")[0].InnerText;
         else
            Console.WriteLine (WikiBot.Msg ("Page \"{0}\" loaded successfully."), Title);
      }

      /// <summary>Loads page text from the specified UTF8-encoded file.</summary>
      /// <param name="filePathName">Path and name of the file.</param>
      public void LoadFromFile (string filePathName)
      {
         var strmReader = new StreamReader (filePathName);
         Text = strmReader.ReadToEnd ();
         strmReader.Close ();
         strmReader.Dispose ();
         Console.WriteLine (
            WikiBot.Msg ("Text for page \"{0}\" successfully loaded from \"{1}\" file."),
            Title, filePathName);
      }

      /// <summary>This function is used internally to gain rights to edit page
      /// on a live wiki site.</summary>
      public void GetEditSessionData ()
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (
               WikiBot.Msg ("No title specified for page to get edit session data."));
         string src = WikiSite.GetPageHtm (WikiSite.IndexPath + "index.php?title=" +
                                          HttpUtility.UrlEncode (Title) + "&action=edit");
         EditSessionTime = RegExes.EditSessionTime.Match (src).Groups[1].ToString ();
         EditSessionToken = RegExes.EditSessionToken.Match (src).Groups[1].ToString ();
         if (string.IsNullOrEmpty (EditSessionToken))
            EditSessionToken = RegExes.EditSessionToken2.Match (src).Groups[1].ToString ();
         IsWatched = Regex.IsMatch (src, "<a href=\"[^\"]+&(amp;)?action=unwatch\"");
      }

      /// <summary>This function is used internally to gain rights to edit page on a live wiki
      /// site. The function queries rights, using bot interface, thus saving traffic.</summary>
      public void GetEditSessionDataEx ()
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (
               WikiBot.Msg ("No title specified for page to get edit session data."));
         string src = WikiSite.GetPageHtm (WikiSite.IndexPath + "api.php?action=query&prop=info" +
                                          "&format=xml&intoken=edit&titles=" + HttpUtility.UrlEncode (Title));
         EditSessionToken = RegExes.EditSessionToken3.Match (src).Groups[1].ToString ();
         if (EditSessionToken == "+\\")
            EditSessionToken = "";
         EditSessionTime = RegExes.EditSessionTime2.Match (src).Groups[1].ToString ();
         if (!string.IsNullOrEmpty (EditSessionTime))
            EditSessionTime = Regex.Replace (EditSessionTime, "\\D", "");
         if (string.IsNullOrEmpty (EditSessionTime) && !string.IsNullOrEmpty (EditSessionToken))
            EditSessionTime = DateTime.Now.ToUniversalTime ().ToString ("yyyyMMddHHmmss");
         if (WikiSite.WatchList == null)
         {
            WikiSite.WatchList = new WikiPageList (WikiSite);
            WikiSite.WatchList.FillFromWatchList ();
         }
         IsWatched = WikiSite.WatchList.Contains (Title);
      }

      /// <summary>Retrieves the title for this Page object using page's numeric revision ID
      /// (also called "oldid"), stored in "lastRevisionID" object's property. Make sure that
      /// "lastRevisionID" property is set before calling this function. Use this function
      /// when working with old revisions to detect if the page was renamed at some
      /// moment.</summary>
      public void GetTitle ()
      {
         if (string.IsNullOrEmpty (LastRevisionId))
            throw new WikiBotException (
               WikiBot.Msg ("No revision ID specified for page to get title for."));
         string src = WikiSite.GetPageHtm (WikiSite.SiteUrl + WikiSite.IndexPath +
                                          "index.php?oldid=" + LastRevisionId);
         Title = Regex.Match (src, "<h1 (?:id=\"firstHeading\" )?class=\"firstHeading\">" +
                                  "(.+?)</h1>").Groups[1].ToString ();
      }

      /// <summary>Saves current contents of page.text on live wiki site. Uses default bot
      /// edit comment and default minor edit mark setting ("true" in most cases)/</summary>
      public void Save ()
      {
         Save (Text, WikiBot.EditComment, WikiBot.IsMinorEdit);
      }

      /// <summary>Saves specified text in page on live wiki. Uses default bot
      /// edit comment and default minor edit mark setting ("true" in most cases).</summary>
      /// <param name="newText">New text for this page.</param>
      public void Save (string newText)
      {
         Save (newText, WikiBot.EditComment, WikiBot.IsMinorEdit);
      }

      /// <summary>Saves current page.text contents on live wiki site.</summary>
      /// <param name="comment">Your edit comment.</param>
      /// <param name="isMinorEdit">Minor edit mark (true = minor edit).</param>
      public void Save (string comment, bool isMinorEdit)
      {
         Save (Text, comment, isMinorEdit);
      }

      /// <summary>Saves specified text in page on live wiki.</summary>
      /// <param name="newText">New text for this page.</param>
      /// <param name="comment">Your edit comment.</param>
      /// <param name="isMinorEdit">Minor edit mark (true = minor edit).</param>
      public void Save (string newText, string comment, bool isMinorEdit)
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to save text to."));
         if (string.IsNullOrEmpty (newText) && string.IsNullOrEmpty (Text))
            throw new WikiBotException (WikiBot.Msg ("No text specified for page to save."));
         if (Text != null && Regex.IsMatch (Text, @"(?is)\{\{(nobots|bots\|(allow=none|" +
                                                 @"deny=(?!none)[^\}]*(" + WikiSite.UserName + @"|all)|optout=all))\}\}"))
            throw new WikiBotException (string.Format (WikiBot.Msg (
               "WikiBot action on \"{0}\" page is prohibited " +
               "by \"nobots\" or \"bots|allow=none\" template."), Title));

         if (WikiBot.UseBotQuery && WikiSite.botQuery &&
             (WikiSite.VersionInfo.Major > 1 || (WikiSite.VersionInfo.Major == 1 && WikiSite.VersionInfo.Minor >= 15)))
            GetEditSessionDataEx ();
         else
            GetEditSessionData ();
         if (string.IsNullOrEmpty (EditSessionTime) || string.IsNullOrEmpty (EditSessionToken))
            throw new WikiBotException (
               string.Format (WikiBot.Msg ("Insufficient rights to edit page \"{0}\"."), Title));
         string postData = string.Format ("wpSection=&wpStarttime={0}&wpEdittime={1}" +
                                         "&wpScrolltop=&wpTextbox1={2}&wpSummary={3}&wpSave=Save%20Page" +
                                         "&wpEditToken={4}{5}{6}",
            // &wpAutoSummary=00000000000000000000000000000000&wpIgnoreBlankSummary=1
                                         DateTime.Now.ToUniversalTime ().ToString ("yyyyMMddHHmmss"),
                                         HttpUtility.UrlEncode (EditSessionTime),
                                         HttpUtility.UrlEncode (newText),
                                         HttpUtility.UrlEncode (comment),
                                         HttpUtility.UrlEncode (EditSessionToken),
                                         IsWatched ? "&wpWatchthis=1" : "",
                                         isMinorEdit ? "&wpMinoredit=1" : "");
         if (WikiBot.AskConfirm)
         {
            Console.WriteLine ();
            Console.WriteLine (WikiBot.Msg ("The following text is going to be saved on page \"{0}\":"), Title);
            Console.WriteLine ();
            Console.WriteLine (Text);
            Console.WriteLine ();
            if (!WikiBot.UserConfirms ())
               return;
         }
         string respStr = WikiSite.PostDataAndGetResultHtm (WikiSite.IndexPath + "index.php?title=" +
                                                           HttpUtility.UrlEncode (Title) + "&action=submit", postData);
         if (respStr.Contains (" name=\"wpTextbox2\""))
            throw new WikiBotException (string.Format (
               WikiBot.Msg ("Edit conflict occurred while trying to savе page \"{0}\"."), Title));
         if (respStr.Contains ("<div class=\"permissions-errors\">"))
            throw new WikiBotException (
               string.Format (WikiBot.Msg ("Insufficient rights to edit page \"{0}\"."), Title));
         if (respStr.Contains ("input name=\"wpCaptchaWord\" id=\"wpCaptchaWord\""))
            throw new WikiBotException (
               string.Format (WikiBot.Msg ("Error occurred when saving page \"{0}\": " +
                                     "WikiBot operation is not allowed for this account at \"{1}\" site."),
                             Title, WikiSite.SiteUrl));
         Console.WriteLine (WikiBot.Msg ("Page \"{0}\" saved successfully."), Title);
         Text = newText;
      }

      /// <summary>Undoes the last edit, so page text reverts to previous contents.
      /// The function doesn't affect other actions like renaming.</summary>
      /// <param name="comment">Revert comment.</param>
      /// <param name="isMinorEdit">Minor edit mark (pass true for minor edit).</param>
      public void Revert (string comment, bool isMinorEdit)
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to revert."));
         var pl = new WikiPageList (WikiSite);
         if (WikiBot.UseBotQuery && WikiSite.botQuery &&
             WikiSite.botQueryVersions.ContainsKey ("ApiQueryRevisions.php"))
            pl.FillFromPageHistoryEx (Title, 2, false);
         else
            pl.FillFromPageHistory (Title, 2);
         if (pl.Count () != 2)
         {
            Console.Error.WriteLine (WikiBot.Msg ("Can't revert page \"{0}\"."), Title);
            return;
         }
         pl[1].Load ();
         Save (pl[1].Text, comment, isMinorEdit);
         Console.WriteLine (WikiBot.Msg ("Page \"{0}\" was reverted."), Title);
      }

      /// <summary>Undoes all last edits of last page contributor, so page text reverts to
      /// previous contents. The function doesn't affect other operations
      /// like renaming or protecting.</summary>
      /// <param name="comment">Revert comment.</param>
      /// <param name="isMinorEdit">Minor edit mark (pass true for minor edit).</param>
      /// <returns>Returns true if last edits were undone.</returns>
      public bool UndoLastEdits (string comment, bool isMinorEdit)
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to revert."));
         var pl = new WikiPageList (WikiSite);
         string lastEditor = "";
         for (int i = 50; i <= 5000; i *= 10)
         {
            if (WikiBot.UseBotQuery && WikiSite.botQuery &&
                WikiSite.botQueryVersions.ContainsKey ("ApiQueryRevisions.php"))
               pl.FillFromPageHistoryEx (Title, i, false);
            else
               pl.FillFromPageHistory (Title, i);
            lastEditor = pl[0].LastUser;
            foreach (WikiPage p in pl)
               if (p.LastUser != lastEditor)
               {
                  p.Load ();
                  Save (p.Text, comment, isMinorEdit);
                  Console.WriteLine (
                     WikiBot.Msg ("Last edits of page \"{0}\" by user {1} were undone."),
                     Title, lastEditor);
                  return true;
               }
            if (pl.Pages.Count < i)
               break;
            pl.Clear ();
         }
         Console.Error.WriteLine (WikiBot.Msg ("Can't undo last edits of page \"{0}\" by user {1}."),
                                 Title, lastEditor);
         return false;
      }

      /// <summary>Protects or unprotects the page, so only authorized group of users can edit or
      /// rename it. Changing page protection mode requires administrator (sysop)
      /// rights.</summary>
      /// <param name="editMode">Protection mode for editing this page (0 = everyone allowed
      /// to edit, 1 = only registered users are allowed, 2 = only administrators are allowed
      /// to edit).</param>
      /// <param name="renameMode">Protection mode for renaming this page (0 = everyone allowed to
      /// rename, 1 = only registered users are allowed, 2 = only administrators
      /// are allowed).</param>
      /// <param name="cascadeMode">In cascading mode, all the pages, included into this page
      /// (e.g., templates or images) are also automatically protected.</param>
      /// <param name="expiryDate">Date and time, expressed in UTC, when protection expires
      /// and page becomes unprotected. Use DateTime.ToUniversalTime() method to convert local
      /// time to UTC, if necessary. Pass DateTime.MinValue to make protection indefinite.</param>
      /// <param name="reason">Reason for protecting this page.</param>
      public void Protect (int editMode, int renameMode, bool cascadeMode,
                          DateTime expiryDate, string reason)
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to protect."));
         string errorMsg =
            WikiBot.Msg ("Only values 0, 1 and 2 are accepted. Please, consult documentation.");
         if (editMode > 2 || editMode < 0)
            throw new ArgumentOutOfRangeException ("editMode", errorMsg);
         if (renameMode > 2 || renameMode < 0)
            throw new ArgumentOutOfRangeException ("renameMode", errorMsg);
         if (expiryDate != DateTime.MinValue && expiryDate < DateTime.Now)
            throw new ArgumentOutOfRangeException ("expiryDate",
                                                  WikiBot.Msg ("Protection expiry date must be hereafter."));
         string res = WikiSite.SiteUrl + WikiSite.IndexPath +
                      "index.php?title=" + HttpUtility.UrlEncode (Title) + "&action=protect";
         string src = WikiSite.GetPageHtm (res);
         EditSessionTime = RegExes.EditSessionTime.Match (src).Groups[1].ToString ();
         EditSessionToken = RegExes.EditSessionToken.Match (src).Groups[1].ToString ();
         if (string.IsNullOrEmpty (EditSessionToken))
            EditSessionToken = RegExes.EditSessionToken2.Match (src).Groups[1].ToString ();
         if (string.IsNullOrEmpty (EditSessionToken))
         {
            Console.Error.WriteLine (
               WikiBot.Msg ("Unable to change protection mode for page \"{0}\"."), Title);
            return;
         }
         string postData = string.Format ("mwProtect-level-edit={0}&mwProtect-level-move={1}" +
                                         "&mwProtect-reason={2}&wpEditToken={3}&mwProtect-expiry={4}{5}",
                                         HttpUtility.UrlEncode (
                                            editMode == 2 ? "sysop" : editMode == 1 ? "autoconfirmed" : ""),
                                         HttpUtility.UrlEncode (
                                            renameMode == 2 ? "sysop" : renameMode == 1 ? "autoconfirmed" : ""),
                                         HttpUtility.UrlEncode (reason),
                                         HttpUtility.UrlEncode (EditSessionToken),
                                         expiryDate == DateTime.MinValue ? "" : expiryDate.ToString ("u"),
                                         cascadeMode ? "&mwProtect-cascade=1" : "");
         string respStr = WikiSite.PostDataAndGetResultHtm (WikiSite.IndexPath +
                                                           "index.php?title=" + HttpUtility.UrlEncode (Title) + "&action=protect", postData);
         if (string.IsNullOrEmpty (respStr))
         {
            Console.Error.WriteLine (
               WikiBot.Msg ("Unable to change protection mode for page \"{0}\"."), Title);
            return;
         }
         Console.WriteLine (
            WikiBot.Msg ("Protection mode for page \"{0}\" changed successfully."), Title);
      }

      /// <summary>Adds page to bot account's watchlist.</summary>
      public void Watch ()
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to watch."));
         string res = WikiSite.SiteUrl + WikiSite.IndexPath +
                      "index.php?title=" + HttpUtility.UrlEncode (Title) + "&action=watch";
         WikiSite.GetPageHtm (res);
         IsWatched = true;
         Console.WriteLine (WikiBot.Msg ("Page \"{0}\" added to watchlist."), Title);
      }

      /// <summary>Removes page from bot account's watchlist.</summary>
      public void Unwatch ()
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to unwatch."));
         string res = WikiSite.SiteUrl + WikiSite.IndexPath +
                      "index.php?title=" + HttpUtility.UrlEncode (Title) + "&action=unwatch";
         WikiSite.GetPageHtm (res);
         IsWatched = false;
         Console.WriteLine (WikiBot.Msg ("Page \"{0}\" was removed from watchlist."), Title);
      }

      /// <summary>This function opens page text in Microsoft Word for editing.
      /// Just close Word after editing, and the revised text will appear in
      /// Page.text variable.</summary>
      /// <remarks>Appropriate PIAs (Primary Interop Assemblies) for available MS Office
      /// version must be installed and referenced in order to use this function. Follow
      /// instructions in "Compile and Run.bat" file to reference PIAs properly in compilation
      /// command, and then recompile the framework. Redistributable PIAs can be downloaded from
      /// http://www.microsoft.com/downloads/results.aspx?freetext=Office%20PIA</remarks>
      public void ReviseInMsWord ()
      {
#if MS_WORD_INTEROP
			if (string.IsNullOrEmpty(text))
				throw new WikiBotException(Bot.Msg("No text on page to revise in Microsoft Word."));
			Microsoft.Office.Interop.Word.Application app =
				new Microsoft.Office.Interop.Word.Application();
			app.Visible = true;
			object mv = System.Reflection.Missing.Value;
			object template = mv;
			object newTemplate = mv;
			object documentType = Microsoft.Office.Interop.Word.WdDocumentType.wdTypeDocument;
			object visible = true;
			Microsoft.Office.Interop.Word.Document doc =
				app.Documents.Add(ref template, ref newTemplate, ref documentType, ref visible);
			doc.Words.First.InsertBefore(text);
			text = null;
			Microsoft.Office.Interop.Word.DocumentEvents_Event docEvents =
				(Microsoft.Office.Interop.Word.DocumentEvents_Event) doc;
			docEvents.Close +=
				new Microsoft.Office.Interop.Word.DocumentEvents_CloseEventHandler(
					delegate { text = doc.Range(ref mv, ref mv).Text; doc.Saved = true; } );
			app.Activate();
			while (text == null);
			text = Regex.Replace(text, "\r(?!\n)", "\r\n");
			app = null;
			doc = null;
			GC.Collect();
			GC.WaitForPendingFinalizers();
			GC.Collect();
			GC.WaitForPendingFinalizers();
			Console.WriteLine(
				Bot.Msg("Text of \"{0}\" page was revised in Microsoft Word."), title);
#else
         throw new WikiBotException (WikiBot.Msg ("Page.ReviseInMSWord() function requires MS " +
                                            "Office PIAs to be installed and referenced. Please, see remarks in function's " +
                                            "documentation in \"Documentation.chm\" file for additional instructions.\n"));
#endif
      }

      /// <summary>Uploads local image to wiki site. Function also works with non-image files.
      /// Note: uploaded image title (wiki page title) will be the same as title of this Page
      /// object, not the title of source file.</summary>
      /// <param name="filePathName">Path and name of local file.</param>
      /// <param name="description">File (image) description.</param>
      /// <param name="license">File license type (may be template title). Used only on
      /// some wiki sites. Pass empty string, if the wiki site doesn't require it.</param>
      /// <param name="copyStatus">File (image) copy status. Used only on some wiki sites. Pass
      /// empty string, if the wiki site doesn't require it.</param>
      /// <param name="source">File (image) source. Used only on some wiki sites. Pass
      /// empty string, if the wiki site doesn't require it.</param>
      public void UploadImage (string filePathName, string description,
                              string license, string copyStatus, string source)
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for image to upload."));
         if (!File.Exists (filePathName))
            throw new WikiBotException (
               string.Format (WikiBot.Msg ("Image file \"{0}\" doesn't exist."), filePathName));
         var fileNameWithoutExtension = Path.GetFileNameWithoutExtension (filePathName);
         if (fileNameWithoutExtension != null && fileNameWithoutExtension.Length < 3)
            throw new WikiBotException (string.Format (WikiBot.Msg ("Name of file \"{0}\" must " +
                                                             "contain at least 3 characters (excluding extension) for successful upload."),
                                                     filePathName));
         Console.WriteLine (WikiBot.Msg ("Uploading image \"{0}\"..."), Title);
         string targetName = WikiSite.RemoveNsPrefix (Title, 6);
         targetName = WikiBot.Capitalize (targetName);
         var webReq = (HttpWebRequest)WebRequest.Create (WikiSite.SiteUrl +
                                                        WikiSite.IndexPath + "index.php?title=" + WikiSite.namespaces["-1"] + ":Upload");
         webReq.Proxy.Credentials = CredentialCache.DefaultCredentials;
         webReq.UseDefaultCredentials = true;
         webReq.Method = "POST";
         string boundary = "----------" + DateTime.Now.Ticks.ToString ("x");
         webReq.ContentType = "multipart/form-data; boundary=" + boundary;
         webReq.UserAgent = WikiBot.BotVer;
         webReq.CookieContainer = WikiSite.cookies;
         if (WikiBot.UnsafeHttpHeaderParsingUsed == 0)
         {
            webReq.ProtocolVersion = HttpVersion.Version10;
            webReq.KeepAlive = false;
         }
         webReq.CachePolicy = new System.Net.Cache.HttpRequestCachePolicy (
            System.Net.Cache.HttpRequestCacheLevel.Refresh);
         var sb = new StringBuilder ();
         string ph = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"";
         sb.Append (ph + "wpIgnoreWarning\"\r\n\r\n1\r\n");
         sb.Append (ph + "wpDestFile\"\r\n\r\n" + targetName + "\r\n");
         sb.Append (ph + "wpUploadAffirm\"\r\n\r\n1\r\n");
         sb.Append (ph + "wpWatchthis\"\r\n\r\n0\r\n");
         sb.Append (ph + "wpUploadCopyStatus\"\r\n\r\n" + copyStatus + "\r\n");
         sb.Append (ph + "wpUploadSource\"\r\n\r\n" + source + "\r\n");
         sb.Append (ph + "wpUpload\"\r\n\r\n" + "upload bestand" + "\r\n");
         sb.Append (ph + "wpLicense\"\r\n\r\n" + license + "\r\n");
         sb.Append (ph + "wpUploadDescription\"\r\n\r\n" + description + "\r\n");
         sb.Append (ph + "wpUploadFile\"; filename=\"" +
                   HttpUtility.UrlEncode (Path.GetFileName (filePathName)) + "\"\r\n" +
                   "Content-Type: application/octet-stream\r\n\r\n");
         byte[] postHeaderBytes = Encoding.UTF8.GetBytes (sb.ToString ());
         byte[] fileBytes = File.ReadAllBytes (filePathName);
         byte[] boundaryBytes = Encoding.ASCII.GetBytes ("\r\n--" + boundary + "\r\n");
         webReq.ContentLength = postHeaderBytes.Length + fileBytes.Length + boundaryBytes.Length;
         Stream reqStream = webReq.GetRequestStream ();
         reqStream.Write (postHeaderBytes, 0, postHeaderBytes.Length);
         reqStream.Write (fileBytes, 0, fileBytes.Length);
         reqStream.Write (boundaryBytes, 0, boundaryBytes.Length);
         WebResponse webResp;
         for (int errorCounter = 0; ; errorCounter++)
         {
            try
            {
               webResp = webReq.GetResponse ();
               break;
            }
            catch (WebException e)
            {
               string message = e.Message;
               if (Regex.IsMatch (message, ": \\(50[02349]\\) "))
               {		// Remote problem
                  if (errorCounter > WikiBot.RetryTimes)
                     throw;
                  Console.Error.WriteLine (message + " " + WikiBot.Msg ("Retrying in 60 seconds."));
                  Thread.Sleep (60000);
               }
               else if (message.Contains ("Section=ResponseStatusLine"))
               {	// Squid problem
                  WikiBot.SwitchUnsafeHttpHeaderParsing (true);
                  UploadImage (filePathName, description, license, copyStatus, source);
                  return;
               }
               else
                  throw;
            }
         }
         var strmReader = new StreamReader (webResp.GetResponseStream ());
         string respStr = strmReader.ReadToEnd ();
         strmReader.Close ();
         strmReader.Dispose ();
         webResp.Close ();
         if (!respStr.Contains (HttpUtility.HtmlEncode (targetName)))
            throw new WikiBotException (string.Format (
               WikiBot.Msg ("Error occurred when uploading image \"{0}\"."), Title));
         try
         {
            string errorMessage = WikiSite.GetMediaWikiMessage ("MediaWiki:Uploadcorrupt");
            if (respStr.Contains (errorMessage))
               throw new WikiBotException (string.Format (
                  WikiBot.Msg ("Error occurred when uploading image \"{0}\"."), Title));
         }
         catch (WikiBotException e)
         {
            if (!e.Message.Contains ("Uploadcorrupt"))	// skip, if MediaWiki message not found
               throw;
         }
         Title = WikiSite.namespaces["6"] + ":" + targetName;
         Text = description;
         Console.WriteLine (WikiBot.Msg ("Image \"{0}\" uploaded successfully."), Title);
      }

      /// <summary>Uploads web image to wiki site.</summary>
      /// <param name="imageFileUrl">Full URL of image file on the web.</param>
      /// <param name="description">Image description.</param>
      /// <param name="license">Image license type. Used only in some wiki sites. Pass
      /// empty string, if the wiki site doesn't require it.</param>
      /// <param name="copyStatus">Image copy status. Used only in some wiki sites. Pass
      /// empty string, if the wiki site doesn't require it.</param>
      public void UploadImageFromWeb (string imageFileUrl, string description,
                                     string license, string copyStatus)
      {
         if (string.IsNullOrEmpty (imageFileUrl))
            throw new WikiBotException (WikiBot.Msg ("No URL specified of image to upload."));
         var res = new Uri (imageFileUrl);
         WikiBot.InitWebClient ();
         string imageFileName = imageFileUrl.Substring (imageFileUrl.LastIndexOf ("/", StringComparison.Ordinal) + 1);
         try
         {
            WikiBot.WebClient.DownloadFile (res, "Cache" + Path.DirectorySeparatorChar + imageFileName);
         }
         catch (WebException)
         {
            throw new WikiBotException (string.Format (
               WikiBot.Msg ("Can't access image \"{0}\"."), imageFileUrl));
         }
         if (!File.Exists ("Cache" + Path.DirectorySeparatorChar + imageFileName))
            throw new WikiBotException (string.Format (
               WikiBot.Msg ("Error occurred when downloading image \"{0}\"."), imageFileUrl));
         UploadImage ("Cache" + Path.DirectorySeparatorChar + imageFileName,
                     description, license, copyStatus, imageFileUrl);
         File.Delete ("Cache" + Path.DirectorySeparatorChar + imageFileName);
      }

      /// <summary>Downloads image, audio or video file, pointed by this page title,
      /// from the wiki site. Redirection is resolved automatically.</summary>
      /// <param name="filePathName">Path and name of local file to save image to.</param>
      public void DownloadImage (string filePathName)
      {
         string res = WikiSite.SiteUrl + WikiSite.IndexPath + "index.php?title=" +
                      HttpUtility.UrlEncode (Title);
         string src;
         try
         {
            src = WikiSite.GetPageHtm (res);
         }
         catch (WebException e)
         {
            string message = e.Message;
            if (message.Contains (": (404) "))
            {		// Not Found
               Console.Error.WriteLine (WikiBot.Msg ("Page \"{0}\" doesn't exist."), Title);
               Text = "";
               return;
            }
            throw;
         }
         var fileLinkRe1 = new Regex ("<a href=\"([^\"]+?)\" class=\"internal\"");
         var fileLinkRe2 =
            new Regex ("<div class=\"fullImageLink\" id=\"file\"><a href=\"([^\"]+?)\"");
         string fileLink;
         if (fileLinkRe1.IsMatch (src))
            fileLink = fileLinkRe1.Match (src).Groups[1].ToString ();
         else if (fileLinkRe2.IsMatch (src))
            fileLink = fileLinkRe2.Match (src).Groups[1].ToString ();
         else
            throw new WikiBotException (string.Format (
               WikiBot.Msg ("Image \"{0}\" doesn't exist."), Title));
         if (!fileLink.StartsWith ("http"))
            fileLink = WikiSite.SiteUrl + fileLink;
         WikiBot.InitWebClient ();
         Console.WriteLine (WikiBot.Msg ("Downloading image \"{0}\"..."), Title);
         WikiBot.WebClient.DownloadFile (fileLink, filePathName);
         Console.WriteLine (WikiBot.Msg ("Image \"{0}\" downloaded successfully."), Title);
      }

      /// <summary>Saves page text to the specified file. If the target file already exists,
      /// it is overwritten.</summary>
      /// <param name="filePathName">Path and name of the file.</param>
      public void SaveToFile (string filePathName)
      {
         if (IsEmpty ())
         {
            Console.Error.WriteLine (WikiBot.Msg ("Page \"{0}\" contains no text to save."), Title);
            return;
         }
         File.WriteAllText (filePathName, Text, Encoding.UTF8);
         Console.WriteLine (WikiBot.Msg ("Text of \"{0}\" page successfully saved in \"{1}\" file."),
                           Title, filePathName);
      }

      /// <summary>Saves page text to the ".txt" file in current directory.
      /// Use Directory.SetCurrentDirectory function to change the current directory (but don't
      /// forget to change it back after saving file). The name of the file is constructed
      /// from the title of the article. Forbidden characters in filenames are replaced
      /// with their Unicode numeric codes (also known as numeric character references
      /// or NCRs).</summary>
      public void SaveToFile ()
      {
         string fileTitle = Title;
         //Path.GetInvalidFileNameChars();
         fileTitle = fileTitle.Replace ("\"", "&#x22;");
         fileTitle = fileTitle.Replace ("<", "&#x3c;");
         fileTitle = fileTitle.Replace (">", "&#x3e;");
         fileTitle = fileTitle.Replace ("?", "&#x3f;");
         fileTitle = fileTitle.Replace (":", "&#x3a;");
         fileTitle = fileTitle.Replace ("\\", "&#x5c;");
         fileTitle = fileTitle.Replace ("/", "&#x2f;");
         fileTitle = fileTitle.Replace ("*", "&#x2a;");
         fileTitle = fileTitle.Replace ("|", "&#x7c;");
         SaveToFile (fileTitle + ".txt");
      }

      /// <summary>Returns true, if page.text field is empty. Don't forget to call
      /// page.Load() before using this function.</summary>
      /// <returns>Returns bool value.</returns>
      public bool IsEmpty ()
      {
         return string.IsNullOrEmpty (Text);
      }

      /// <summary>Returns true, if page.text field is not empty. Don't forget to call
      /// Load() or LoadEx() before using this function.</summary>
      /// <returns>Returns bool value.</returns>
      public bool Exists ()
      {
         return !string.IsNullOrEmpty (Text);
      }

      /// <summary>Returns true, if page redirects to another page. Don't forget to load
      /// actual page contents from live wiki "Page.Load()" before using this function.</summary>
      /// <returns>Returns bool value.</returns>
      public bool IsRedirect ()
      {
         if (!Exists ())
            return false;
         return WikiSite.RedirectRe.IsMatch (Text);
      }

      /// <summary>Returns redirection target. Don't forget to load
      /// actual page contents from live wiki "Page.Load()" before using this function.</summary>
      /// <returns>Returns redirection target page title as string. Or empty string, if this
      /// Page object does not redirect anywhere.</returns>
      public string RedirectsTo ()
      {
         if (IsRedirect ())
            return WikiSite.RedirectRe.Match (Text).Groups[1].ToString ().Trim ();
         return string.Empty;
      }

      /// <summary>If this page is a redirection, this function loads the title and text
      /// of redirected-to page into this Page object.</summary>
      public void ResolveRedirect ()
      {
         if (IsRedirect ())
         {
            LastRevisionId = null;
            Title = RedirectsTo ();
            Load ();
         }
      }

      /// <summary>Returns true, if this page is a disambiguation page. Don't forget to load
      /// actual page contents from live wiki  before using this function. Local redirect
      /// templates of Wikimedia sites are also recognized, but if this extended functionality
      /// is undesirable, then just set appropriate disambiguation template's title in
      /// "disambigStr" variable of Site object. Use "|" as a delimiter when enumerating
      /// several templates in "disambigStr" variable.</summary>
      /// <returns>Returns bool value.</returns>
      public bool IsDisambig ()
      {
         if (string.IsNullOrEmpty (Text))
            return false;
         if (!string.IsNullOrEmpty (WikiSite.DisambigStr))
            return Regex.IsMatch (Text, @"(?i)\{\{(" + WikiSite.DisambigStr + ")}}");
         Console.WriteLine (WikiBot.Msg ("Initializing disambiguation template tags..."));
         WikiSite.DisambigStr = "disambiguation|disambig|dab";
         var res = new Uri ("http://en.wikipedia.org/w/index.php?title=Template:Disambig/doc" +
                           "&action=raw&ctype=text/plain&dontcountme=s");
         string buffer = Text;
         Text = WikiBot.GetWebResource (res, "");
         string[] iw = GetInterWikiLinks ();
         foreach (string s in iw)
            if (s.StartsWith (WikiSite.language + ":"))
            {
               WikiSite.DisambigStr += "|" + s.Substring (s.LastIndexOf (":", StringComparison.Ordinal) + 1,
                                                         s.Length - s.LastIndexOf (":", StringComparison.Ordinal) - 1);
               break;
            }
         Text = buffer;
         return Regex.IsMatch (Text, @"(?i)\{\{(" + WikiSite.DisambigStr + ")}}");
      }

      /// <summary>This internal function removes the namespace prefix from page title.</summary>
      public void RemoveNsPrefix ()
      {
         Title = WikiSite.RemoveNsPrefix (Title, 0);
      }

      /// <summary>Function changes default English namespace prefixes to correct local prefixes
      /// (e.g. for German wiki sites it changes "Category:..." to "Kategorie:...").</summary>
      public void CorrectNsPrefix ()
      {
         Title = WikiSite.CorrectNsPrefix (Title);
      }

      /// <summary>Returns the array of strings, containing all wikilinks ([[...]])
      /// found in page text, excluding links in image descriptions, but including
      /// interwiki links, links to sister projects, categories, images, etc.</summary>
      /// <returns>Returns raw links in strings array.</returns>
      public string[] GetAllLinks ()
      {
         MatchCollection matches = RegExes.WikiLink.Matches (Text);
         var matchStrings = new string[matches.Count];
         for (int i = 0; i < matches.Count; i++)
            matchStrings[i] = matches[i].Groups[1].Value;
         return matchStrings;
      }

      /// <summary>Finds all internal wikilinks in page text, excluding interwiki
      /// links, links to sister projects, categories, embedded images and links in
      /// image descriptions.</summary>
      /// <returns>Returns the WikiPageList object, in which page titles are the wikilinks,
      /// found in text.</returns>
      public WikiPageList GetLinks ()
      {
         MatchCollection matches = RegExes.WikiLink.Matches (Text);
         var exclLinks = new StringCollection ();
         exclLinks.AddRange (GetInterWikiLinks ());
         exclLinks.AddRange (GetSisterWikiLinks (true));
         var pl = new WikiPageList (WikiSite) { ListName = "Links" };
         for (int i = 0; i < matches.Count; i++)
         {
            string str = matches[i].Groups[1].Value;
            if (str.StartsWith (WikiSite.namespaces["6"] + ":", true, WikiSite.langCulture) ||
                str.StartsWith (WikiSite.wikiNSpaces["6"] + ":", true, WikiSite.langCulture) ||
                str.StartsWith (WikiSite.namespaces["14"] + ":", true, WikiSite.langCulture) ||
                str.StartsWith (WikiSite.wikiNSpaces["14"] + ":", true, WikiSite.langCulture))
               continue;
            str = str.TrimStart (':');
            if (exclLinks.Contains (str))
               continue;
            int fragmentPosition = str.IndexOf ("#", StringComparison.Ordinal);
            if (fragmentPosition != -1)
               str = str.Substring (0, fragmentPosition);
            pl.Add (new WikiPage (WikiSite, str));
         }
         return pl;
      }

      /// <summary>Returns the array of strings, containing external links,
      /// found in page text.</summary>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetExternalLinks ()
      {
         MatchCollection matches = RegExes.WebLink.Matches (Text);
         var matchStrings = new string[matches.Count];
         for (int i = 0; i < matches.Count; i++)
            matchStrings[i] = matches[i].Value;
         return matchStrings;
      }

      /// <summary>Returns the array of strings, containing interwiki links,
      /// found in page text. But no displayed links are returned,
      /// like [[:de:Stern]] - these are returned by GetSisterWikiLinks(true)
      /// function. Interwiki links are returned without square brackets.</summary>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetInterWikiLinks ()
      {
         return GetInterWikiLinks (false);
      }

      /// <summary>Returns the array of strings, containing interwiki links,
      /// found in page text. Displayed links like [[:de:Stern]] are not returned,
      /// these are returned by GetSisterWikiLinks(true) function.</summary>
      /// <param name="inSquareBrackets">Pass "true" to get interwiki links
      ///in square brackets, for example "[[de:Stern]]", otherwise the result
      /// will be like "de:Stern", without brackets.</param>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetInterWikiLinks (bool inSquareBrackets)
      {
         if (string.IsNullOrEmpty (WikiSite.WMLangsStr))
            WikiSite.GetWikimediaWikisList ();
         MatchCollection matches = RegExes.InterWikiLink.Matches (Text);
         var matchStrings = new string[matches.Count];
         for (int i = 0; i < matches.Count; i++)
         {
            matchStrings[i] = matches[i].Groups[1].Value;
            if (inSquareBrackets)
               matchStrings[i] = "[[" + matchStrings[i] + "]]";
         }
         return matchStrings;
      }

      /// <summary>Adds interwiki links to the page. It doesn't remove or replace old
      /// interwiki links, this can be done by calling RemoveInterWikiLinks function
      /// or manually, if necessary.</summary>
      /// <param name="iwikiLinks">Interwiki links as an array of strings, with or
      /// without square brackets, for example: "de:Stern" or "[[de:Stern]]".</param>
      public void AddInterWikiLinks (string[] iwikiLinks)
      {
         if (iwikiLinks.Length == 0)
            throw new ArgumentNullException ("iwikiLinks");
         var iwList = new List<string> (iwikiLinks);
         AddInterWikiLinks (iwList);
      }

      /// <summary>Adds interwiki links to the page. It doesn't remove or replace old
      /// interwiki links, this can be done by calling RemoveInterWikiLinks function
      /// or manually, if necessary.</summary>
      /// <param name="iwikiLinks">Interwiki links as List of strings, with or
      /// without square brackets, for example: "de:Stern" or "[[de:Stern]]".</param>
      public void AddInterWikiLinks (List<string> iwikiLinks)
      {
         if (iwikiLinks.Count == 0)
            throw new ArgumentNullException ("iwikiLinks");
         if (iwikiLinks.Count == 1 && iwikiLinks[0] == null)
            iwikiLinks.Clear ();
         for (int i = 0; i < iwikiLinks.Count; i++)
            iwikiLinks[i] = iwikiLinks[i].Trim ("[]\f\n\r\t\v ".ToCharArray ());
         iwikiLinks.AddRange (GetInterWikiLinks ());
         SortInterWikiLinks (ref iwikiLinks);
         RemoveInterWikiLinks ();
         Text += "\r\n";
         foreach (string str in iwikiLinks)
            Text += "\r\n[[" + str + "]]";
      }

      /// <summary>Sorts interwiki links in page text according to site rules.
      /// Only rules for some Wikipedia projects are implemented so far.
      /// In other cases links are ordered alphabetically.</summary>
      public void SortInterWikiLinks ()
      {
         AddInterWikiLinks (new string[] { null });
      }

      /// <summary>This internal function sorts interwiki links in page text according 
      /// to site rules. Only rules for some Wikipedia projects are implemented
      /// so far. In other cases links are ordered alphabetically.</summary>
      /// <param name="iwList">Interwiki links without square brackets in
      /// List object, either ordered or unordered.</param>
      public void SortInterWikiLinks (ref List<string> iwList)
      {
         string[] iwikiLinksOrder = null;
         if (iwList.Count < 2)
            return;
         switch (WikiSite.SiteUrl)
         {		// special sort orders
            case "http://en.wikipedia.org":
            case "http://simple.wikipedia.org":
            case "http://be-x-old.wikipedia.org":
            case "http://lb.wikipedia.org":
            case "http://mk.wikipedia.org":
            case "http://no.wikipedia.org":
            case "http://pl.wikipedia.org": iwikiLinksOrder = WikiSite.iwikiLinksOrderByLocal; break;
            case "http://ms.wikipedia.org":
            case "http://et.wikipedia.org":
            case "http://vi.wikipedia.org":
            case "http://fi.wikipedia.org": iwikiLinksOrder = WikiSite.iwikiLinksOrderByLocalFW; break;
            case "http://sr.wikipedia.org": iwikiLinksOrder = WikiSite.iwikiLinksOrderByLatinFW; break;
            default: iwList.Sort (); break;
         }
         if (iwikiLinksOrder == null)
            return;
         var sortedIwikiList = new List<string> ();
         foreach (string iwikiLang in iwikiLinksOrder)
         {
            string prefix = iwikiLang + ":";
            sortedIwikiList.AddRange (iwList.Where (iwikiLink => iwikiLink.StartsWith (prefix)));
         }
         foreach (string iwikiLink in iwList)
            if (!sortedIwikiList.Contains (iwikiLink))
               sortedIwikiList.Add (iwikiLink);
         iwList = sortedIwikiList;
         switch (WikiSite.SiteUrl)
         {		// special sort orders, based on default iwList.Sort();
            case "http://hu.wikipedia.org":
            case "http://he.wikipedia.org": iwList.Remove ("en"); iwList.Insert (0, "en"); break;
            case "http://nn.wikipedia.org":
               iwList.Remove ("no"); iwList.Remove ("sv"); iwList.Remove ("da");
               iwList.InsertRange (0, new[] { "no", "sv", "da" }); break;
            case "http://te.wikipedia.org":
               iwList.Remove ("en"); iwList.Remove ("hi"); iwList.Remove ("kn");
               iwList.Remove ("ta"); iwList.Remove ("ml");
               iwList.InsertRange (0, new[] { "en", "hi", "kn", "ta", "ml" }); break;
            case "http://yi.wikipedia.org":
               iwList.Remove ("en"); iwList.Remove ("he"); iwList.Remove ("de");
               iwList.InsertRange (0, new[] { "en", "he", "de" }); break;
            case "http://ur.wikipedia.org":
               iwList.Remove ("ar"); iwList.Remove ("fa"); iwList.Remove ("en");
               iwList.InsertRange (0, new[] { "ar", "fa", "en" }); break;
         }
      }

      /// <summary>Removes all interwiki links from text of page.</summary>
      public void RemoveInterWikiLinks ()
      {
         if (string.IsNullOrEmpty (WikiSite.WMLangsStr))
            WikiSite.GetWikimediaWikisList ();
         Text = RegExes.InterWikiLink.Replace (Text, "");
         Text = Text.TrimEnd ("\r\n".ToCharArray ());
      }

      /// <summary>Returns the array of strings, containing links to sister Wikimedia
      /// Foundation Projects, found in page text.</summary>
      /// <param name="includeDisplayedInterWikiLinks">Include displayed interwiki
      /// links like "[[:de:Stern]]".</param>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetSisterWikiLinks (bool includeDisplayedInterWikiLinks)
      {
         if (string.IsNullOrEmpty (WikiSite.WMLangsStr))
            WikiSite.GetWikimediaWikisList ();
         MatchCollection sisterMatches = RegExes.SisterWikiLink.Matches (Text);
         MatchCollection iwikiMatches = RegExes.InterWikiDisplayedLink.Matches (Text);
         int size = includeDisplayedInterWikiLinks ?
                                                      sisterMatches.Count + iwikiMatches.Count : sisterMatches.Count;
         var matchStrings = new string[size];
         int i = 0;
         for (; i < sisterMatches.Count; i++)
            matchStrings[i] = sisterMatches[i].Groups[1].Value;
         if (includeDisplayedInterWikiLinks)
            for (int j = 0; j < iwikiMatches.Count; i++, j++)
               matchStrings[i] = iwikiMatches[j].Groups[1].Value;
         return matchStrings;
      }

      /// <summary>Function converts basic HTML markup in page text to wiki
      /// markup, except for tables markup, that is left unchanged. Use
      /// ConvertHtmlTablesToWikiTables function to convert HTML
      /// tables markup to wiki format.</summary>
      public void ConvertHtmlMarkupToWikiMarkup ()
      {
         Text = Regex.Replace (Text, "(?is)n?<(h1)( [^/>]+?)?>(.+?)</\\1>n?", "\n= $3 =\n");
         Text = Regex.Replace (Text, "(?is)n?<(h2)( [^/>]+?)?>(.+?)</\\1>n?", "\n== $3 ==\n");
         Text = Regex.Replace (Text, "(?is)n?<(h3)( [^/>]+?)?>(.+?)</\\1>n?", "\n=== $3 ===\n");
         Text = Regex.Replace (Text, "(?is)n?<(h4)( [^/>]+?)?>(.+?)</\\1>n?", "\n==== $3 ====\n");
         Text = Regex.Replace (Text, "(?is)n?<(h5)( [^/>]+?)?>(.+?)</\\1>n?",
                              "\n===== $3 =====\n");
         Text = Regex.Replace (Text, "(?is)n?<(h6)( [^/>]+?)?>(.+?)</\\1>n?",
                              "\n====== $3 ======\n");
         Text = Regex.Replace (Text, "(?is)\n?\n?<p( [^/>]+?)?>(.+?)</p>", "\n\n$2");
         Text = Regex.Replace (Text, "(?is)<a href ?= ?[\"'](http:[^\"']+)[\"']>(.+?)</a>",
                              "[$1 $2]");
         Text = Regex.Replace (Text, "(?i)</?(b|strong)>", "'''");
         Text = Regex.Replace (Text, "(?i)</?(i|em)>", "''");
         Text = Regex.Replace (Text, "(?i)\n?<hr ?/?>\n?", "\n----\n");
         Text = Regex.Replace (Text, "(?i)<(hr|br)( [^/>]+?)? ?/?>", "<$1$2 />");
      }

      /// <summary>Function converts HTML table markup in page text to wiki
      /// table markup.</summary>
      public void ConvertHtmlTablesToWikiTables ()
      {
         if (!Text.Contains ("</table>"))
            return;
         Text = Regex.Replace (Text, ">\\s+<", "><");
         Text = Regex.Replace (Text, "<table( ?[^>]*)>", "\n{|$1\n");
         Text = Regex.Replace (Text, "</table>", "|}\n");
         Text = Regex.Replace (Text, "<caption( ?[^>]*)>", "|+$1 | ");
         Text = Regex.Replace (Text, "</caption>", "\n");
         Text = Regex.Replace (Text, "<tr( ?[^>]*)>", "|-$1\n");
         Text = Regex.Replace (Text, "</tr>", "\n");
         Text = Regex.Replace (Text, "<th([^>]*)>", "!$1 | ");
         Text = Regex.Replace (Text, "</th>", "\n");
         Text = Regex.Replace (Text, "<td([^>]*)>", "|$1 | ");
         Text = Regex.Replace (Text, "</td>", "\n");
         Text = Regex.Replace (Text, "\n(\\||\\|\\+|!) \\| ", "\n$1 ");
         Text = Text.Replace ("\n\n|", "\n|");
      }

      /// <summary>Returns the array of strings, containing category names found in
      /// page text with namespace prefix, but without sorting keys. Use the result
      /// strings to call FillFromCategory(string) or FillFromCategoryTree(string)
      /// function. Categories, added by templates, are not returned. Use GetAllCategories
      /// function to get such categories too.</summary>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetCategories ()
      {
         return GetCategories (true, false);
      }

      /// <summary>Returns the array of strings, containing category names found in
      /// page text. Categories, added by templates, are not returned. Use GetAllCategories
      /// function to get categories added by templates too.</summary>
      /// <param name="withNameSpacePrefix">If true, function returns strings with
      /// namespace prefix like "Category:Stars", not just "Stars".</param>
      /// <param name="withSortKey">If true, function returns strings with sort keys,
      /// if found. Like "Stars|D3" (in [[Category:Stars|D3]]), not just "Stars".</param>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetCategories (bool withNameSpacePrefix, bool withSortKey)
      {
         MatchCollection matches = WikiSite.WikiCategoryRe.Matches (
            Regex.Replace (Text, "(?is)<nowiki>.+?</nowiki>", ""));
         var matchStrings = new string[matches.Count];
         for (int i = 0; i < matches.Count; i++)
         {
            matchStrings[i] = matches[i].Groups[4].Value;
            if (withSortKey)
               matchStrings[i] += matches[i].Groups[5].Value;
            if (withNameSpacePrefix)
               matchStrings[i] = WikiSite.namespaces["14"] + ":" + matchStrings[i];
         }
         return matchStrings;
      }

      /// <summary>
      /// Gets a list of all categories in the format of a page list.
      /// </summary>
      /// <returns>Page list of all categories.</returns>
      public WikiPageList GetAllCategoriesAsPageList ()
      {
         var categories = this.GetAllCategories ();
         return new WikiPageList (this.WikiSite, categories) { ListName = "Categories" };
      }

      /// <summary>Returns the array of strings, containing category names found in
      /// page text and added by page's templates. Categories are returned  with
      /// namespace prefix, but without sorting keys. Use the result strings
      /// to call FillFromCategory(string) or FillFromCategoryTree(string).</summary>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetAllCategories ()
      {
         return GetAllCategories (true);
      }

      /// <summary>Returns the array of strings, containing category names found in
      /// page text and added by page's templates.</summary>
      /// <param name="withNameSpacePrefix">If true, function returns strings with
      /// namespace prefix like "Category:Stars", not just "Stars".</param>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetAllCategories (bool withNameSpacePrefix)
      {
         string uri;
        if (WikiBot.UseBotQuery && WikiSite.botQuery && WikiSite.VersionInfo >= new Version (1, 15))
            uri = WikiSite.SiteUrl + WikiSite.IndexPath +
                  "api.php?action=query&prop=categories" +
                  "&clprop=sortkey|hidden&cllimit=5000&format=xml";//"&titles=" +
                  //HttpUtility.UrlEncode (Title);
         else
            uri = WikiSite.SiteUrl + WikiSite.IndexPath + "index.php?title=" +
                  HttpUtility.UrlEncode (Title) + "&redirect=no";

         string xpathQuery;
         if (WikiBot.UseBotQuery && WikiSite.botQuery && WikiSite.VersionInfo >= new Version (1, 15))
            xpathQuery = "//categories/cl/@title";
         else if (WikiSite.VersionInfo >= new Version (1, 13))
            xpathQuery = "//ns:div[ @id='mw-normal-catlinks' or @id='mw-hidden-catlinks' ]" +
                         "/ns:span/ns:a";
         else
            xpathQuery = "//ns:div[ @id='catlinks' ]/ns:p/ns:span/ns:a";

         string src = WikiSite.GetPageHtm (uri);
         if (WikiBot.UseBotQuery != true || WikiSite.botQuery != true || WikiSite.VersionInfo < new Version (1, 15))
         {
            int startPos = src.IndexOf ("<!-- start content -->", StringComparison.Ordinal);
            int endPos = src.IndexOf ("<!-- end content -->", StringComparison.Ordinal);
            if (startPos != -1 && endPos != -1 && startPos < endPos)
               src = src.Remove (startPos, endPos - startPos);
            else
            {
               startPos = src.IndexOf ("<!-- bodytext -->", StringComparison.Ordinal);
               endPos = src.IndexOf ("<!-- /bodytext -->", StringComparison.Ordinal);
               if (startPos != -1 && endPos != -1 && startPos < endPos)
                  src = src.Remove (startPos, endPos - startPos);
            }
         }

         XPathNodeIterator iterator = WikiSite.GetXmlIterator (src, xpathQuery);
         var matchStrings = new string[iterator.Count];
         iterator.MoveNext ();
         for (int i = 0; i < iterator.Count; i++)
         {
            if (iterator.Current != null)
               matchStrings[i] = (withNameSpacePrefix ? WikiSite.namespaces["14"] + ":" : "") +
                                 WikiSite.RemoveNsPrefix (HttpUtility.HtmlDecode (iterator.Current.Value), 14);
            iterator.MoveNext ();
         }

         return matchStrings;
      }

      /// <summary>Adds the page to the specified category by adding
      /// link to that category in page text. If the link to the specified category
      /// already exists, the function does nothing.</summary>
      /// <param name="categoryName">Category name, with or without prefix.
      /// Sort key can also be included after "|", like "Category:Stars|D3".</param>
      public void AddToCategory (string categoryName)
      {
         if (string.IsNullOrEmpty (categoryName))
            throw new ArgumentNullException ("categoryName");
         categoryName = WikiSite.RemoveNsPrefix (categoryName, 14);
         string cleanCategoryName = !categoryName.Contains ("|") ? categoryName.Trim ()
                                       : categoryName.Substring (0, categoryName.IndexOf ('|')).Trim ();
         string[] categories = GetCategories (false, false);
         if (categories.Any (category => category == WikiBot.Capitalize (cleanCategoryName) ||
                                         category == WikiBot.Uncapitalize (cleanCategoryName)))
         {
            return;
         }
         string[] iw = GetInterWikiLinks ();
         RemoveInterWikiLinks ();
         Text += (categories.Length == 0 ? "\r\n" : "") +
                 "\r\n[[" + WikiSite.namespaces["14"] + ":" + categoryName + "]]\r\n";
         if (iw.Length != 0)
            AddInterWikiLinks (iw);
         Text = Text.TrimEnd ("\r\n".ToCharArray ());
      }

      /// <summary>Removes the page from category by deleting link to that category in
      /// page text.</summary>
      /// <param name="categoryName">Category name, with or without prefix.</param>
      public void RemoveFromCategory (string categoryName)
      {
         if (string.IsNullOrEmpty (categoryName))
            throw new ArgumentNullException ("categoryName");
         categoryName = WikiSite.RemoveNsPrefix (categoryName, 14).Trim ();
         categoryName = !categoryName.Contains ("|") ? categoryName
                           : categoryName.Substring (0, categoryName.IndexOf ('|'));
         string[] categories = GetCategories (false, false);
         if (Array.IndexOf (categories, WikiBot.Capitalize (categoryName)) == -1 &&
             Array.IndexOf (categories, WikiBot.Uncapitalize (categoryName)) == -1)
            return;
         string regexCategoryName = Regex.Escape (categoryName);
         regexCategoryName = regexCategoryName.Replace ("_", "\\ ").Replace ("\\ ", "[_\\ ]");
         int firstCharIndex = (regexCategoryName[0] == '\\') ? 1 : 0;
         regexCategoryName = "[" + char.ToLower (regexCategoryName[firstCharIndex]) +
                             char.ToUpper (regexCategoryName[firstCharIndex]) + "]" +
                             regexCategoryName.Substring (firstCharIndex + 1);
         Text = Regex.Replace (Text, @"\[\[((?i)" + WikiSite.namespaces["14"] + "|" +
                                    WikiSite.wikiNSpaces["14"] + "):" + regexCategoryName + @"(\|.*?)?]]\r?\n?", "");
         Text = Text.TrimEnd ("\r\n".ToCharArray ());
      }

      /// <summary>Returns the array of strings, containing titles of templates, found on page.
      /// The "msgnw:" template modifier is not returned.
      /// Links to templates (like [[:Template:...]]) are not returned. Templates,
      /// mentioned inside &lt;nowiki&gt;&lt;/nowiki&gt; tags are also not returned. The
      /// "magic words" (see http://meta.wikimedia.org/wiki/Help:Magic_words) are recognized and
      /// not returned by this function as templates. When using this function on text of the
      /// template, parameters names and numbers (like {{{link}}} and {{{1}}}) are not returned
      /// by this function as templates too.</summary>
      /// <param name="withNameSpacePrefix">If true, function returns strings with
      /// namespace prefix like "Template:SomeTemplate", not just "SomeTemplate".</param>
      /// <returns>Returns the string[] array. Duplicates are possible.</returns>
      public string[] GetTemplates (bool withNameSpacePrefix)
      {
         string str = RegExes.NoWikiMarkup.Replace (Text, "");
         if (GetNamespace () == 10)
            str = Regex.Replace (str, @"\{\{\{.*?}}}", "");
         MatchCollection matches = Regex.Matches (str, @"(?s)\{\{(.+?)(}}|\|)");
         var matchStrings = new string[matches.Count];
         int j = 0;
         for (int i = 0; i < matches.Count; i++)
         {
            string match = matches[i].Groups[1].Value;
            string matchLowerCase = match.ToLower ();
            if (WikiSite.mediaWikiVars.Any (mediaWikiVar => matchLowerCase == mediaWikiVar))
            {
               match = "";
            }
            if (string.IsNullOrEmpty (match))
               continue;
            if (WikiSite.parserFunctions.Any (matchLowerCase.StartsWith))
            {
               match = "";
            }
            if (string.IsNullOrEmpty (match))
               continue;
            if (match.StartsWith ("msgnw:") && match.Length > 6)
               match = match.Substring (6);
            match = WikiSite.RemoveNsPrefix (match, 10).Trim ();
            if (withNameSpacePrefix)
               matchStrings[j++] = WikiSite.namespaces["10"] + ":" + match;
            else
               matchStrings[j++] = match;
         }
         Array.Resize (ref matchStrings, j);
         return matchStrings;
      }

      /// <summary>Returns the array of strings, containing templates, found on page
      /// Everything inside braces is returned with all parameters
      /// untouched. Links to templates (like [[:Template:...]]) are not returned. Templates,
      /// mentioned inside &lt;nowiki&gt;&lt;/nowiki&gt; tags are also not returned. The
      /// "magic words" (see http://meta.wikimedia.org/wiki/Help:Magic_words) are recognized and
      /// not returned by this function as templates. When using this function on text of the
      /// template (on [[Template:NNN]] page), parameters names and numbers (like {{{link}}} 
      /// and {{{1}}}) are not returned by this function as templates too.</summary>
      /// <returns>Returns the string[] array.</returns>
      public string[] GetTemplatesWithParams ()
      {
         var templPos = new Dictionary<int, int> ();
         var templates = new StringCollection ();
         int startPos;
         string str = Text;
         while ((startPos = str.LastIndexOf ("{{", StringComparison.Ordinal)) != -1)
         {
            int endPos = str.IndexOf ("}}", startPos, StringComparison.Ordinal);
            int len = (endPos != -1) ? endPos - startPos + 2 : 2;
            if (len != 2)
               templPos.Add (startPos, len);
            str = str.Remove (startPos, len);
            str = str.Insert (startPos, new String ('_', len));
         }
         string[] templTitles = GetTemplates (false);
         Array.Reverse (templTitles);
         foreach (KeyValuePair<int, int> pos in templPos)
            templates.Add (Text.Substring (pos.Key + 2, pos.Value - 4));
         for (int i = 0; i < templTitles.Length; i++)
            while (i < templates.Count &&
                   !templates[i].StartsWith (templTitles[i]) &&
                   !templates[i].StartsWith (WikiSite.namespaces["10"] + ":" +
                                            templTitles[i], true, WikiSite.langCulture) &&
                   !templates[i].StartsWith (WikiSite.wikiNSpaces["10"] + ":" +
                                            templTitles[i], true, WikiSite.langCulture) &&
                   !templates[i].StartsWith ("msgnw:" + templTitles[i]))
               templates.RemoveAt (i);
         var arr = new string[templates.Count];
         templates.CopyTo (arr, 0);
         Array.Reverse (arr);
         return arr;
      }

      /// <summary>Adds a specified template to the end of the page text
      /// (right before categories).</summary>
      /// <param name="templateText">Complete template in double brackets,
      /// e.g. "{{TemplateTitle|param1=val1|param2=val2}}".</param>
      public void AddTemplate (string templateText)
      {
         if (string.IsNullOrEmpty (templateText))
            throw new ArgumentNullException ("templateText");
         var templateInsertion = new Regex ("([^}]\n|}})\n*\\[\\[((?i)" +
                                           Regex.Escape (WikiSite.namespaces["14"].ToString ()) + "|" +
                                           Regex.Escape (WikiSite.wikiNSpaces["14"].ToString ()) + "):");
         if (templateInsertion.IsMatch (Text))
            Text = templateInsertion.Replace (Text, "$1\n" + templateText + "\n\n[[" +
                                                   WikiSite.namespaces["14"] + ":", 1);
         else
         {
            string[] iw = GetInterWikiLinks ();
            RemoveInterWikiLinks ();
            Text += "\n\n" + templateText;
            if (iw.Length != 0)
               AddInterWikiLinks (iw);
            Text = Text.TrimEnd ("\r\n".ToCharArray ());
         }
      }

      /// <summary>Removes all instances of a specified template from page text.</summary>
      /// <param name="templateTitle">Title of template to remove.</param>
      public void RemoveTemplate (string templateTitle)
      {
         if (string.IsNullOrEmpty (templateTitle))
            throw new ArgumentNullException ("templateTitle");
         templateTitle = Regex.Escape (templateTitle);
         templateTitle = "(" + Char.ToUpper (templateTitle[0]) + "|" +
                         Char.ToLower (templateTitle[0]) + ")" +
                         (templateTitle.Length > 1 ? templateTitle.Substring (1) : "");
         Text = Regex.Replace (Text, @"(?s)\{\{\s*" + templateTitle +
                                    @"(.*?)}}\r?\n?", "");
      }

      /// <summary>Returns specified parameter of a specified template. If several instances
      /// of specified template are found in text of this page, all parameter values
      /// are returned.</summary>
      /// <param name="templateTitle">Title of template to get parameter of.</param>
      /// <param name="templateParameter">Title of template's parameter. If parameter is
      /// untitled, specify it's number as string. If parameter is titled, but it's number is
      /// specified, the function will return empty List &lt;string&gt; object.</param>
      /// <returns>Returns the List &lt;string&gt; object with strings, containing values of
      /// specified parameters in all found template instances. Returns empty List &lt;string&gt;
      /// object if no specified template parameters were found.</returns>
      public List<string> GetTemplateParameter (string templateTitle, string templateParameter)
      {
         if (string.IsNullOrEmpty (templateTitle))
            throw new ArgumentNullException ("templateTitle");
         if (string.IsNullOrEmpty (templateParameter))
            throw new ArgumentNullException ("templateParameter");
         if (string.IsNullOrEmpty (Text))
            throw new InvalidOperationException ("Text cannot be null or empty.");

         var parameterValues = new List<string> ();
         templateTitle = templateTitle.Trim ();
         templateParameter = templateParameter.Trim ();
         var templateTitleRegex = new Regex ("^\\s*(" +
                                            WikiBot.Capitalize (Regex.Escape (templateTitle)) + "|" +
                                            WikiBot.Uncapitalize (Regex.Escape (templateTitle)) +
                                            ")\\s*\\|");
         foreach (string template in GetTemplatesWithParams ())
         {
            if (templateTitleRegex.IsMatch (template))
            {
               Dictionary<string, string> parameters = WikiSite.ParseTemplate (template);
               if (parameters.ContainsKey (templateParameter))
                  parameterValues.Add (parameters[templateParameter]);
            }
         }
         return parameterValues;
      }

      /// <summary>This helper method returns specified parameter of a first found instance of
      /// specified template. If no such template or no such parameter was found,
      /// empty string "" is returned.</summary>
      /// <param name="templateTitle">Title of template to get parameter of.</param>
      /// <param name="templateParameter">Title of template's parameter. If parameter is
      /// untitled, specify it's number as string. If parameter is titled, but it's number is
      /// specified, the function will return empty List &lt;string&gt; object.</param>
      /// <returns>Returns parameter as string or empty string "".</returns>
      /// <remarks>Thanks to Eyal Hertzog and metacafe.com team for idea of this
      /// function.</remarks>
      public string GetFirstTemplateParameter (string templateTitle, string templateParameter)
      {
         List<string> paramsList = GetTemplateParameter (templateTitle, templateParameter);
         if (paramsList.Count == 0)
            return "";
         return paramsList[0];
      }

      /// <summary>Sets the specified parameter of the specified template to new value.
      /// If several instances of specified template are found in text of this page, either
      /// first value can be set, or all values in all instances.</summary>
      /// <param name="templateTitle">Title of template.</param>
      /// <param name="templateParameter">Title of template's parameter.</param>
      /// <param name="newParameterValue">New value to set the parameter to.</param>
      /// <param name="firstTemplateOnly">When set to true, only first found template instance
      /// is modified. When set to false, all found template instances are modified.</param>
      /// <returns>Returns the number of modified values.</returns>
      /// <remarks>Thanks to Eyal Hertzog and metacafe.com team for idea of this
      /// function.</remarks>
      public int SetTemplateParameter (string templateTitle, string templateParameter,
                                      string newParameterValue, bool firstTemplateOnly)
      {
         if (string.IsNullOrEmpty (templateTitle))
            throw new ArgumentNullException ("templateTitle");
         if (string.IsNullOrEmpty (templateParameter))
            throw new ArgumentNullException ("templateParameter");
         if (string.IsNullOrEmpty (templateParameter))
            throw new ArgumentNullException ("newParameterValue");
         if (string.IsNullOrEmpty (Text))
            throw new InvalidOperationException ("Text cannot be null or empty.");

         int i = 0;
         templateTitle = templateTitle.Trim ();
         templateParameter = templateParameter.Trim ();
         var templateTitleRegex = new Regex ("^\\s*(" +
                                            WikiBot.Capitalize (Regex.Escape (templateTitle)) + "|" +
                                            WikiBot.Uncapitalize (Regex.Escape (templateTitle)) +
                                            ")\\s*\\|");
         foreach (string template in GetTemplatesWithParams ())
         {
            if (templateTitleRegex.IsMatch (template))
            {
               Dictionary<string, string> parameters = WikiSite.ParseTemplate (template);
               parameters[templateParameter] = newParameterValue;
               var oldTemplate = new Regex (Regex.Escape (template));
               string newTemplate = WikiSite.FormatTemplate (templateTitle, parameters, template);
               newTemplate = newTemplate.Substring (2, newTemplate.Length - 4);
               Text = oldTemplate.Replace (Text, newTemplate, 1);
               i++;
               if (firstTemplateOnly)
                  break;
            }
         }
         return i;
      }

      /// <summary>Returns the array of strings, containing names of files,
      /// embedded in page, including images in galleries (inside "gallery" tag).
      /// But no links to images and files, like [[:Image:...]] or [[:File:...]] or
      /// [[Media:...]].</summary>
      /// <param name="withNameSpacePrefix">If true, function returns strings with
      /// namespace prefix like "Image:Example.jpg" or "File:Example.jpg",
      /// not just "Example.jpg".</param>
      /// <returns>Returns the string[] array. The array can be empty (of size 0). Strings in
      /// array may recur, indicating that file was mentioned several times on the page.</returns>
      public string[] GetImages (bool withNameSpacePrefix)
      {
         return GetImagesEx (withNameSpacePrefix, false);
      }

      /// <summary>Returns the array of strings, containing names of files,
      /// mentioned on a page.</summary>
      /// <param name="withNameSpacePrefix">If true, function returns strings with
      /// namespace prefix like "Image:Example.jpg" or "File:Example.jpg",
      /// not just "Example.jpg".</param>
      /// <param name="includeFileLinks">If true, function also returns links to images,
      /// like [[:Image:...]] or [[:File:...]] or [[Media:...]]</param>
      /// <returns>Returns the string[] array. The array can be empty (of size 0).Strings in
      /// array may recur, indicating that file was mentioned several times on the page.</returns>
      public string[] GetImagesEx (bool withNameSpacePrefix, bool includeFileLinks)
      {
         if (string.IsNullOrEmpty (Text))
            throw new InvalidOperationException ("Text cannot be null or empty.");
         string nsPrefixes = "File|Image|" + Regex.Escape (WikiSite.namespaces["6"].ToString ());
         if (includeFileLinks)
         {
            nsPrefixes += "|" + Regex.Escape (WikiSite.namespaces["-2"].ToString ()) + "|" +
                          Regex.Escape (WikiSite.wikiNSpaces["-2"].ToString ());
         }
         MatchCollection matches;
         if (Regex.IsMatch (Text, "(?is)<gallery>.*</gallery>"))
            matches = Regex.Matches (Text, "(?i)" + (includeFileLinks ? "" : "(?<!:)") +
                                          "(" + nsPrefixes + ")(:)(.*?)(\\||\r|\n|]])");		// FIXME: inexact matches
         else
            matches = Regex.Matches (Text, @"\[\[" + (includeFileLinks ? ":?" : "") +
                                          "(?i)((" + nsPrefixes + @"):(.+?))(\|(.+?))*?]]");
         var matchStrings = new string[matches.Count];
         for (int i = 0; i < matches.Count; i++)
         {
            if (withNameSpacePrefix)
               matchStrings[i] = WikiSite.namespaces["6"] + ":" + matches[i].Groups[3].Value;
            else
               matchStrings[i] = matches[i].Groups[3].Value;
         }
         return matchStrings;
      }

      /// <summary>Identifies the namespace of the page.</summary>
      /// <returns>Returns the integer key of the namespace.</returns>
      public int GetNamespace ()
      {
         Title = Title.TrimStart (new char[] { ':' });
         foreach (DictionaryEntry ns in WikiSite.namespaces)
         {
            if (Title.StartsWith (ns.Value + ":", true, WikiSite.langCulture))
               return int.Parse (ns.Key.ToString ());
         }
         return (from DictionaryEntry ns in WikiSite.wikiNSpaces where Title.StartsWith (ns.Value + ":", true, WikiSite.langCulture) select int.Parse (ns.Key.ToString ())).FirstOrDefault ();
      }

      /// <summary>Sends page title to console.</summary>
      public void ShowTitle ()
      {
         Console.WriteLine ();
         Console.WriteLine (WikiBot.Msg ("The title of this page is \"{0}\"."), Title);
      }

      /// <summary>Sends page text to console.</summary>
      public void ShowText ()
      {
         Console.WriteLine ();
         Console.WriteLine (WikiBot.Msg ("The text of \"{0}\" page:"), Title);
         Console.WriteLine ();
         Console.WriteLine (Text);
         Console.WriteLine ();
      }

      /// <summary>Renames the page.</summary>
      /// <param name="newTitle">New title of that page.</param>
      /// <param name="reason">Reason for renaming.</param>
      public void RenameTo (string newTitle, string reason)
      {
         if (string.IsNullOrEmpty (newTitle))
            throw new ArgumentNullException ("newTitle");
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to rename."));
         //Page mp = new Page(site, "Special:Movepage/" + HttpUtility.UrlEncode(title));
         var mp = new WikiPage (WikiSite, "Special:Movepage/" + Title);
         mp.GetEditSessionData ();
         if (string.IsNullOrEmpty (mp.EditSessionToken))
            throw new WikiBotException (string.Format (
               WikiBot.Msg ("Unable to rename page \"{0}\" to \"{1}\"."), Title, newTitle));
         if (WikiBot.AskConfirm)
         {
            Console.Write ("\n\n" +
                          WikiBot.Msg ("The page \"{0}\" is going to be renamed to \"{1}\".\n"),
                          Title, newTitle);
            if (!WikiBot.UserConfirms ())
               return;
         }
         string postData = string.Format ("wpNewTitle={0}&wpOldTitle={1}&wpEditToken={2}" +
                                         "&wpReason={3}", HttpUtility.UrlEncode (newTitle), HttpUtility.UrlEncode (Title),
                                         HttpUtility.UrlEncode (mp.EditSessionToken), HttpUtility.UrlEncode (reason));
         string respStr = WikiSite.PostDataAndGetResultHtm (WikiSite.IndexPath +
                                                           "index.php?title=Special:Movepage&action=submit", postData);
         if (RegExes.EditSessionToken2.IsMatch (respStr))
            throw new WikiBotException (string.Format (
               WikiBot.Msg ("Failed to rename page \"{0}\" to \"{1}\"."), Title, newTitle));
         Console.WriteLine (
            WikiBot.Msg ("Page \"{0}\" was successfully renamed to \"{1}\"."), Title, newTitle);
         Title = newTitle;
      }

      /// <summary>Deletes the page. Sysop rights are needed to delete page.</summary>
      /// <param name="reason">Reason for deleting.</param>
      public void Delete (string reason)
      {
         if (string.IsNullOrEmpty (Title))
            throw new WikiBotException (WikiBot.Msg ("No title specified for page to delete."));
         string respStr1 = WikiSite.GetPageHtm (WikiSite.IndexPath + "index.php?title=" +
                                               HttpUtility.UrlEncode (Title) + "&action=delete");
         EditSessionToken = RegExes.EditSessionToken.Match (respStr1).Groups[1].ToString ();
         if (string.IsNullOrEmpty (EditSessionToken))
            EditSessionToken = RegExes.EditSessionToken2.Match (respStr1).Groups[1].ToString ();
         if (string.IsNullOrEmpty (EditSessionToken))
            throw new WikiBotException (
               string.Format (WikiBot.Msg ("Unable to delete page \"{0}\"."), Title));
         if (WikiBot.AskConfirm)
         {
            Console.WriteLine ();
            Console.WriteLine (WikiBot.Msg ("The page \"{0}\" is going to be deleted."), Title);
            if (!WikiBot.UserConfirms ())
               return;
         }
         string postData = string.Format ("wpReason={0}&wpEditToken={1}",
                                         HttpUtility.UrlEncode (reason), HttpUtility.UrlEncode (EditSessionToken));
         string respStr2 = WikiSite.PostDataAndGetResultHtm (WikiSite.IndexPath + "index.php?title=" +
                                                            HttpUtility.UrlEncode (Title) + "&action=delete", postData);
         if (RegExes.EditSessionToken2.IsMatch (respStr2))
            throw new WikiBotException (
               string.Format (WikiBot.Msg ("Failed to delete page \"{0}\"."), Title));
         Console.WriteLine (WikiBot.Msg ("Page \"{0}\" was successfully deleted."), Title);
         Title = "";
      }

      /// <summary>
      /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
      /// </summary>
      /// <returns>
      /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
      /// </returns>
      /// <filterpriority>2</filterpriority>
      public override string ToString ()
      {
         return this.Title;
      }
   }
}